
class EventState {

    skipNextObservers!: boolean;
    mask!: number;
    target?: any;
    currentTarget?: any;
    lastReturnValue?: any;
    userInfo?: any;

    constructor(mask: number, skipNextObservers?: boolean, target?: any, currentTarget?: any) {
        this.initialize(mask, skipNextObservers, target, currentTarget);
    }

    initialize(mask: number, skipNextObservers?: boolean, target?: any, currentTarget?: any): EventState {
        this.mask = mask;
        this.skipNextObservers = skipNextObservers as boolean;
        this.target = target;
        this.currentTarget = currentTarget;
        return this;
    }

}

class Observer<T> {
    callback: (eventData: T, eventState: EventState) => void;
    mask: number;
    scope: any;
    _willBeUnregistered: boolean;
    unregisterOnNextCall: boolean;
    _remove: { (): void } | null;

    constructor(callback: (eventData: T, eventState: EventState) => void, mask: number, scope?: any) {
        this.callback = callback;
        this.mask = mask;
        this.scope = scope;
        this._willBeUnregistered = false;
        this.unregisterOnNextCall = false;
        this._remove = null;
    }

    remove(): void {
        if (this._remove) {
            this._remove();
        }
    }
}
class Observable<T> {
    notifyIfTriggered: boolean;
    private _observers;
    private _numObserversMarkedAsDeleted;
    private _hasNotified;
    private _lastNotifiedValue?: T;
    _eventState: EventState;
    private _onObserverAdded?: { (observer: Observer<T>): void } | null;

    static FromPromise<T, E = Error>(promise: Promise<T>, onErrorObservable?: Observable<E>): Observable<T> {
        const observable = new Observable<T>();
        promise
            .then((ret) => {
                observable.notifyObservers(ret);
            })
            .catch((err) => {
                if (onErrorObservable) {
                    onErrorObservable.notifyObservers(err);
                }
                else {
                    throw err;
                }
            });
        return observable;
    }

    get observers(): Array<Observer<T>> {
        return this._observers;
    }

    constructor(onObserverAdded?: (observer: Observer<T>) => void, notifyIfTriggered?: boolean) {
        this.notifyIfTriggered = notifyIfTriggered as boolean;
        this._observers = new Array();
        this._numObserversMarkedAsDeleted = 0;
        this._hasNotified = false;
        this._eventState = new EventState(0);
        if (onObserverAdded) {
            this._onObserverAdded = onObserverAdded;
        }
    }

    add(callback?: ((eventData: T, eventState: EventState) => void) | null | undefined, mask: number = -1, insertFirst?: boolean, scope?: any, unregisterOnFirstCall?: boolean): Observer<T> | null {
        if (!callback) {
            return null;
        }
        const observer = new Observer(callback, mask, scope);
        observer.unregisterOnNextCall = unregisterOnFirstCall as boolean;
        if (insertFirst) {
            this._observers.unshift(observer);
        }
        else {
            this._observers.push(observer);
        }
        if (this._onObserverAdded) {
            this._onObserverAdded(observer);
        }

        if (this._hasNotified && this.notifyIfTriggered) {
            if (this._lastNotifiedValue !== undefined) {
                this.notifyObserver(observer, this._lastNotifiedValue);
            }
        }
        observer._remove = () => {
            this.remove(observer);
        };
        return observer;
    }

    addOnce(callback?: ((eventData: T, eventState: EventState) => void) | null | undefined): null | Observer<T> {
        return this.add(callback, undefined, undefined, undefined, true);
    }

    remove(observer: Observer<T> | null): boolean {
        if (!observer) {
            return false;
        }
        observer._remove = null;
        const index = this._observers.indexOf(observer);
        if (index !== -1) {
            this._deferUnregister(observer);
            return true;
        }
        return false;
    }

    removeCallback(callback: (eventData: T, eventState: EventState) => void, scope?: any): boolean {
        for (let index = 0; index < this._observers.length; index++) {
            const observer = this._observers[index];
            if (observer._willBeUnregistered) {
                continue;
            }
            if (observer.callback === callback && (!scope || scope === observer.scope)) {
                this._deferUnregister(observer);
                return true;
            }
        }
        return false;
    }

    _deferUnregister(observer: Observer<T>): void {
        if (observer._willBeUnregistered) {
            return;
        }
        this._numObserversMarkedAsDeleted++;
        observer.unregisterOnNextCall = false;
        observer._willBeUnregistered = true;
        setTimeout(() => {
            this._remove(observer);
        }, 0);
    }

    private _remove(observer: Observer<T>, updateCounter = true): boolean {
        if (!observer) {
            return false;
        }
        const index = this._observers.indexOf(observer);
        if (index !== -1) {
            if (updateCounter) {
                this._numObserversMarkedAsDeleted--;
            }
            this._observers.splice(index, 1);
            return true;
        }
        return false;
    }

    makeObserverTopPriority(observer: Observer<T>): void {
        this._remove(observer, false);
        this._observers.unshift(observer);
    }

    makeObserverBottomPriority(observer: Observer<T>): void {
        this._remove(observer, false);
        this._observers.push(observer);
    }

    notifyObservers(eventData: T, mask: number = -1, target?: any, currentTarget?: any, userInfo?: any): boolean {
        if (this.notifyIfTriggered) {
            this._hasNotified = true;
            this._lastNotifiedValue = eventData;
        }
        if (!this._observers.length) {
            return true;
        }
        const state = this._eventState;
        state.mask = mask;
        state.target = target;
        state.currentTarget = currentTarget;
        state.skipNextObservers = false;
        state.lastReturnValue = eventData;
        state.userInfo = userInfo;
        for (const obs of this._observers) {
            if (obs._willBeUnregistered) {
                continue;
            }
            if (obs.mask & mask as number) {
                if (obs.unregisterOnNextCall) {
                    this._deferUnregister(obs);
                }
                if (obs.scope) {
                    state.lastReturnValue = obs.callback.apply(obs.scope, [eventData, state]);
                }
                else {
                    state.lastReturnValue = obs.callback(eventData, state);
                }
            }
            if (state.skipNextObservers) {
                return false;
            }
        }
        return true;
    }

    notifyObserver(observer: Observer<T>, eventData: T, mask: number = -1): void {
        if (this.notifyIfTriggered) {
            this._hasNotified = true;
            this._lastNotifiedValue = eventData;
        }
        if (observer._willBeUnregistered) {
            return;
        }
        const state = this._eventState;
        state.mask = mask;
        state.skipNextObservers = false;
        if (observer.unregisterOnNextCall) {
            this._deferUnregister(observer);
        }
        observer.callback(eventData, state);
    }

    hasObservers(): boolean {
        return this._observers.length - this._numObserversMarkedAsDeleted > 0;
    }

    clear(): void {
        while (this._observers.length) {
            const o = this._observers.pop();
            if (o) {
                o._remove = null;
            }
        }
        this._onObserverAdded = null;
        this._numObserversMarkedAsDeleted = 0;
        this.cleanLastNotifiedState();
    }

    cleanLastNotifiedState(): void {
        this._hasNotified = false;
        this._lastNotifiedValue = undefined;
    }

    clone(): Observable<T> {
        const result = new Observable<T>();
        result._observers = this._observers.slice(0);
        return result;
    }

    hasSpecificMask(mask: number = -1): boolean {
        for (const obs of this._observers) {
            if (obs.mask & mask || obs.mask === mask) {
                return true;
            }
        }
        return false;
    }
}


export { EventState, Observer, Observable };